#pragma once

#include <sstream>
#include <string>
#include <exception>
#include <vector>
#include <tuple>
#include <memory>
#include <typeinfo>
#define FMT_HEADER_ONLY
#include "fmt/core.h"
#include <mysql/mysql.h>

namespace easymysql
{

struct except : std::exception
{
    except(const std::string& s)
        : _s("easymysql::" + s)
    {}

    virtual const char* what() const throw()
    {
        return _s.c_str();
    }

private:
    std::string _s;
};


template<typename value_type>
class optional
{
public:
    optional() = default;

    optional(value_type&& val)
        : _val(new value_type(std::move(val)))
    {}

    optional(const value_type&) = delete;
    optional& operator=(const value_type&) = delete;

    operator bool()
    {
        return !_val;
    }

    value_type& operator*()
    {
        if (!_val)
            throw std::runtime_error("has no value");

        return *_val;
    }

private:
    std::unique_ptr<value_type> _val;
};


class fmtr
{
public:
    template<typename... Args>
    static std::string format(const std::string& pattern, Args&&... args)
    {
        return fmt::format(pattern, std::forward<Args>(args)...);
    }
};



class connection
{
public:
    connection(const std::string& host, const std::string& user, const std::string& passwd, const std::string& db, uint16_t port)
    {
        if ((_conn = mysql_init(nullptr)) == nullptr)
        {
            throw except("mysql init failed");
        }

        if (mysql_real_connect(_conn, host.c_str(), user.c_str(), passwd.c_str(), db.c_str(), port, nullptr, 0) == nullptr)
        {
            mysql_close(_conn);
            throw except(std::string("connect failed: ") + mysql_error(_conn));
        }
    }

    ~connection()
    {
        if (_conn)
            mysql_close(_conn);
    }

    MYSQL* handler() const
    {
        return _conn;
    }

    unsigned long long insert_id()
    {
        return mysql_insert_id(_conn);
    }

    // Can check the return value to see the update or insert query is successful or not,
    // especially when it contains where clause.
    unsigned long long affected_rows()
    {
        return mysql_affected_rows(_conn);
    }

private:
    MYSQL* _conn;
};

class result
{
public:
    result(MYSQL* handler)
        : _hdr(handler)
        , _pres(nullptr)
    {
        store_result();
    }

    result(const connection& conn)
        : _hdr(conn.handler())
        , _pres(nullptr)
    {
        store_result();
    }

    template<typename tuple>
    bool get(tuple* container)
    {
        if (!_pres)
            return false;

        MYSQL_ROW mysql_row = mysql_fetch_row(_pres);
        if (mysql_row == nullptr)
            return false;

        std::vector<std::string> row = trans_row(mysql_row, mysql_num_fields(_pres));

        tuple_parser<tuple, std::tuple_size<tuple>::value - 1>::parser(*container, row);
        return true;
    }

    template<typename tuple>
    optional<tuple> get()
    {
        if (!_pres)
            return optional<tuple>();

        MYSQL_ROW mysql_row = mysql_fetch_row(_pres);
        if (mysql_row == nullptr)
            return optional<tuple>();

        std::vector<std::string> row = trans_row(mysql_row, mysql_num_fields(_pres));

        tuple container;
        tuple_parser<tuple, std::tuple_size<tuple>::value - 1>::parser(container, row);
        return optional<tuple>(std::move(container));
    }

    // template<typename tuple>
    std::vector<std::string> get_field_names()
    {
        std::vector<std::string> ret;

        MYSQL_FIELD* fields = mysql_fetch_fields(_pres);
        int          cols = mysql_num_fields(_pres);

        for (int i = 0; i < cols; ++i)
            ret.push_back(fields[i].name);

        return ret;
    }

    // Judge the point `_res` is nullptr or not, only work for select query.
    // If the query is not a select query, you should get the result of `mysql_query` in `inqury`.
    bool query_succ()
    {
        return _pres;
    }

    ~result()
    {
        mysql_free_result(_pres);
    }

private:
    template<class tuple, std::size_t N>
    struct tuple_parser
    {
        using value_type = typename std::tuple_element<N, tuple>::type;

        static void parser(tuple& container, const std::vector<std::string>& values)
        {
            tuple_parser<tuple, N - 1>::parser(container, values);
            std::get<N>(container) = to_type<value_type>(values[N]);
        }
    };

    // Specialized versions are used to terminate recursion.
    template<class tuple>
    struct tuple_parser<tuple, 0>
    {
        using value_type = typename std::tuple_element<0, tuple>::type;

        static void parser(tuple& container, const std::vector<std::string>& values)
        {
            std::get<0>(container) = to_type<value_type>(values[0]);
        }
    };

    bool store_result()
    {
        return (_pres = mysql_store_result(_hdr));
    }

    std::vector<std::string> trans_row(const MYSQL_ROW row, int size)
    {
        std::vector<std::string> v;

        for (int i = 0; i < size; ++i)
            v.push_back(row[i]);

        return v;
    }

    template<typename value_type>
    static value_type to_type(const std::string& s)
    {
        value_type value;
        if (!(std::istringstream(s) >> value))
        {
            throw except(std::string("conversion failed value_type: ") + typeid(value_type).name());
        }
        return value;
    }

private:
    MYSQL*     _hdr;
    MYSQL_RES* _pres;
};

class executer
{
public:
    executer(MYSQL* handler)
        : _conn(handler)
    {}

    executer(const connection& conn)
        : _conn(conn.handler())
    {}

    template<typename... Args>
    result operator()(const std::string& query, Args&&... args)
    {
        return execute(query, std::forward<Args>(args)...);
    }

    template<typename... Args>
    result execute(const std::string& query, Args&&... args)
    {
        inquire(fmtr::format(query, std::forward<Args>(args)...));
        return result(_conn);
    }

    unsigned long long insert_id()
    {
        return mysql_insert_id(_conn);
    }

    unsigned long long affected_rows()
    {
        return mysql_affected_rows(_conn);
    }

private:
    void inquire(const std::string& query)
    {
        if (mysql_query(_conn, query.c_str()))
        {
            throw except(std::string("execute failed: ") + mysql_error(_conn));
        }
    }

private:
    MYSQL* _conn;
};

} // namespace easymysql